package com.katesoft.scale4j.persistent.client;

import static org.hibernate.criterion.Restrictions.eq;

import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceException;

import net.jcip.annotations.ThreadSafe;

import org.hibernate.Criteria;
import org.hibernate.EntityMode;
import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.StaleObjectStateException;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Restrictions;
import org.hibernate.ejb.HibernateEntityManagerFactory;
import org.hibernate.metadata.ClassMetadata;
import org.perf4j.aop.Profiled;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.HibernateOptimisticLockingFailureException;
import org.springframework.orm.hibernate3.HibernateTemplate;
import org.springframework.orm.jpa.JpaCallback;
import org.springframework.orm.jpa.JpaTemplate;

import com.katesoft.scale4j.persistent.model.unified.AbstractPersistentEntity;
import com.katesoft.scale4j.persistent.model.unified.BO;
import com.katesoft.scale4j.persistent.model.unified.IBO;

/**
 * Default implementation of IHibernateOperations interface.
 * <p/>
 * find methods marked with Profiled annotation, so runtime performance static can be gathered.
 * jvmcluster-persistent does not actually track performance statistic, but jvmcluster-rttp contains
 * performance aspect, so execution of methods are tracked by pef4j framework.
 * <p/>
 * NOTE:
 * <p/>
 * 1) exposeNativeSession flag is always true, user overrides are suppressed(method for setting this
 * property is not final, you can override if needed).
 * <p/>
 * 2) allowCreate flag is always true, user overrides are suppressed(method for setting this
 * property is not final, you can override if needed).
 * <p/>
 * 3) checkWriteOperations flag is always false, user overrides are suppressed(method for setting
 * this property is not final, you can override if needed).
 * <p/>
 * 4) alwaysUseNewSession flag is always false, user overrides are suppressed(method for setting
 * this property is not final, you can override if needed).
 * 
 * @author kate2007
 */
@ThreadSafe
public class LocalHibernateTemplate extends HibernateTemplate implements IHibernateOperations {
   private JpaTemplate jpaTemplate;

   public LocalHibernateTemplate() {
      super();
   }

   public LocalHibernateTemplate(HibernateEntityManagerFactory entityManagerFactory) {
      this();
      logger.info(String.format("LocalHibernateTemplate initialized with JPA context = %s",
               entityManagerFactory));
      this.jpaTemplate = new JpaTemplate(entityManagerFactory);
      this.jpaTemplate.setExposeNativeEntityManager(true);
      this.setSessionFactory(entityManagerFactory.getSessionFactory());
   }

   public LocalHibernateTemplate(SessionFactory sessionFactory) {
      this();
      logger.info(String.format("LocalHibernateTemplate initialized with native context = %s",
               sessionFactory));
      super.setExposeNativeSession(true);
      super.setAllowCreate(true);
      super.setCheckWriteOperations(false);
      super.setAlwaysUseNewSession(false);
      super.setSessionFactory(sessionFactory);
   }

   @Override
   public <T> T execute(JpaCallback<T> action) throws DataAccessException {
      ensureJpaIsUsed();
      return jpaTemplate.execute(action);
   }

   @SuppressWarnings("rawtypes")
   @Override
   public List executeFind(JpaCallback<?> action) throws DataAccessException {
      ensureJpaIsUsed();
      return jpaTemplate.executeFind(action);
   }

   @Override
   public <T> T find(Class<T> entityClass, Object id) throws DataAccessException {
      ensureJpaIsUsed();
      return jpaTemplate.find(entityClass, id);
   }

   @Override
   public <T> T getReference(Class<T> entityClass, Object id) throws DataAccessException {
      ensureJpaIsUsed();
      return jpaTemplate.getReference(entityClass, id);
   }

   @Override
   public void remove(Object entity) throws DataAccessException {
      ensureJpaIsUsed();
      jpaTemplate.remove(entity);
   }

   @SuppressWarnings("rawtypes")
   @Override
   @Profiled
   public List findByNamedParams(String queryString, Map<String, ?> params)
            throws DataAccessException {
      ensureJpaIsUsed();
      return jpaTemplate.findByNamedParams(queryString, params);
   }

   @SuppressWarnings("rawtypes")
   @Override
   @Profiled
   public List findByNamedQueryAndNamedParams(String queryName, Map<String, ?> params)
            throws DataAccessException {
      ensureJpaIsUsed();
      return jpaTemplate.findByNamedQueryAndNamedParams(queryName, params);
   }

   @Override
   protected <T> T doExecute(final HibernateCallback<T> action, boolean enforceNewSession,
            boolean enforceNativeSession) throws DataAccessException {
      if (jpaTemplate != null) {
         return jpaTemplate.execute(new JpaCallback<T>() {
            @Override
            public T doInJpa(EntityManager em) throws PersistenceException {
               Session session = em.unwrap(Session.class);
               try {
                  return action.doInHibernate(session);
               } catch (HibernateException ex) {
                  DataAccessException dataAccessException = convertHibernateAccessException(ex);
                  throw new PersistenceException(dataAccessException);
               } catch (SQLException ex) {
                  DataAccessException dataAccessException = convertJdbcAccessException(ex);
                  throw new PersistenceException(dataAccessException);
               }
            }
         });
      } else {
         return super.doExecute(action, enforceNewSession, enforceNativeSession);
      }
   }

   @Override
   public <T extends AbstractPersistentEntity> T findEntityForUpdate(final Class<T> entityClass,
            final long uniqueIdentifier, long expectedVersion) throws DataAccessException {
      T result = execute(new HibernateCallback<T>() {
         @SuppressWarnings({ "unchecked" })
         @Override
         public T doInHibernate(Session session) throws HibernateException, SQLException {
            Criteria criteria = session.createCriteria(entityClass);
            criteria.add(Restrictions.eq(BO.PROP_UID, uniqueIdentifier));
            return (T) criteria.uniqueResult();
         }
      });
      if (result == null) {
         throw new EmptyResultDataAccessException(String.format(
                  "unable to find %s using unique_identifier %s", entityClass, uniqueIdentifier), 1);
      }
      if (result.getVersion() != expectedVersion) {
         throw new HibernateOptimisticLockingFailureException(new StaleObjectStateException(
                  entityClass.getName(), uniqueIdentifier));
      }
      return result;
   }

   @Override
   @Profiled(
             tag = "hibernate.loadAll_{$0.name}",
             message = "loadAll called with parameters[class={$0.name} forceAttributesLoad={$1}]")
   public <T extends AbstractPersistentEntity> List<T> loadAll(final Class<T> entityClass,
            final boolean forceLoad) {
      return execute(new HibernateCallback<List<T>>() {
         @SuppressWarnings({ "unchecked" })
         @Override
         public List<T> doInHibernate(Session session) throws HibernateException, SQLException {
            List<T> list = session.createCriteria(entityClass).list();
            for (T next : list) {
               if (forceLoad)
                  next.forceAttributesLoad();
            }
            return list;
         }
      });
   }

   @Override
   protected void prepareQuery(final Query queryObject) {
      super.prepareQuery(queryObject);
   }

   @Override
   protected void prepareCriteria(final Criteria criteria) {
      super.prepareCriteria(criteria);
   }

   @Override
   @Profiled(tag = "hibernate.flush")
   public void flush() throws DataAccessException {
      super.flush();
   }

   @Override
   @Profiled(
             tag = "hibernate.loadAll_{$0.name}",
             message = "loadAll called with parameters[class={$0.name}]")
   public <T> List<T> loadAll(Class<T> entityClass) throws DataAccessException {
      return super.loadAll(entityClass);
   }

   @Override
   @Profiled(
             tag = "hibernate.deleteAll_{$0.name}",
             message = "deleteAll called with parameters[class={$0.name}]")
   public void deleteAll(final Class<?> clazz) {
      execute(new HibernateCallback<Object>() {
         @Override
         public Object doInHibernate(Session session) throws HibernateException, SQLException {
            for (Object next : session.createCriteria(clazz).list()) {
               session.delete(next);
            }
            session.flush();
            return null;
         }
      });
   }

   @Override
   public <T extends AbstractPersistentEntity> T loadByGlobalUniqueIdentifier(
            final Class<T> persistentClazz, final String guid, final boolean forceDirtyLoad) {
      return execute(new HibernateCallback<T>() {
         @SuppressWarnings({ "unchecked" })
         @Override
         public T doInHibernate(Session session) throws HibernateException, SQLException {
            T t = (T) session.createCriteria(persistentClazz).add(eq(BO.PROP_GUID, guid))
                     .uniqueResult();
            if (forceDirtyLoad && t != null) {
               t.forceAttributesLoad();
            }
            return t;
         }
      });
   }

   @Override
   @SuppressWarnings({ "unchecked", "rawtypes" })
   public <T extends AbstractPersistentEntity> T loadByGlobalUniqueIdentifier(final String guid,
            final boolean forceDirtyLoad) {
      for (Entry<String, ClassMetadata> entry : getSessionFactory().getAllClassMetadata()
               .entrySet()) {
         Class clazz = entry.getValue().getMappedClass(EntityMode.POJO);
         if (IBO.class.isAssignableFrom(clazz)) {
            AbstractPersistentEntity candidate = loadByGlobalUniqueIdentifier(clazz, guid,
                     forceDirtyLoad);
            if (candidate != null) {
               return (T) candidate;
            }
         }
      }
      return null;
   }

   @SuppressWarnings("rawtypes")
   @Override
   @Profiled(
             tag = "hibernate.findByNamedParam",
             message = "findByNamedParam called with parameters[queryString={$0} paramName={$1} value={$2}] and returned {$return.size} elements")
   public List findByNamedParam(String queryString, String paramName, Object value)
            throws DataAccessException {
      return super.findByNamedParam(queryString, paramName, value);
   }

   @SuppressWarnings("rawtypes")
   @Override
   @Profiled(
             tag = "hibernate.findByNamedParam",
             message = "findByNamedParam called with parameters[queryString={$0} paramNames={$1} values={$2}] and returned {$return.size} "
                      + "elements")
   public List findByNamedParam(String queryString, String[] paramNames, Object[] values)
            throws DataAccessException {
      return super.findByNamedParam(queryString, paramNames, values);
   }

   @SuppressWarnings("rawtypes")
   @Override
   @Profiled(
             tag = "hibernate.findByValueBean",
             message = "findByValueBean called with parameters[queryString={$0} valueBean={$1}] and returned {$return.size} elements")
   public List findByValueBean(String queryString, Object valueBean) throws DataAccessException {
      return super.findByValueBean(queryString, valueBean);
   }

   @SuppressWarnings("rawtypes")
   @Override
   @Profiled(
             tag = "hibernate.findByNamedQuery_{$0}",
             message = "findByNamedQuery called with parameters[queryName={$0}] and returned {$return.size} elements")
   public List findByNamedQuery(String queryName) throws DataAccessException {
      return super.findByNamedQuery(queryName);
   }

   @SuppressWarnings("rawtypes")
   @Override
   @Profiled(
             tag = "jvmcluster.hibernate.findByNamedQuery_{$0}",
             message = "findByNamedQuery called with parameters[queryName={$0} value={$1}] and returned {$return.size} elements")
   public List findByNamedQuery(String queryName, Object value) throws DataAccessException {
      return super.findByNamedQuery(queryName, value);
   }

   @SuppressWarnings("rawtypes")
   @Override
   @Profiled(
             tag = "hibernate.findByNamedQuery_{$0}",
             message = "findByNamedQuery called with parameters[queryName={$0} values={$1}] and returned {$return.size} elements")
   public List findByNamedQuery(String queryName, Object... values) throws DataAccessException {
      return super.findByNamedQuery(queryName, values);
   }

   @SuppressWarnings("rawtypes")
   @Override
   @Profiled(
             tag = "hibernate.findByNamedQueryAndNamedParam_{$0}",
             message = "findByNamedQueryAndNamedParam called with parameters[queryName={$0} paramName={$1} value={$2}] and returned {$return.size} "
                      + "elements")
   public List findByNamedQueryAndNamedParam(String queryName, String paramName, Object value)
            throws DataAccessException {
      return super.findByNamedQueryAndNamedParam(queryName, paramName, value);
   }

   @SuppressWarnings("rawtypes")
   @Override
   @Profiled(
             tag = "hibernate.findByNamedQueryAndNamedParam_{$0}",
             message = "findByNamedQueryAndNamedParam called with parameters[queryName={$0} paramNames={$1} values={$2}] and returned {$return.size} "
                      + "elements")
   public List findByNamedQueryAndNamedParam(String queryName, String[] paramNames, Object[] values)
            throws DataAccessException {
      return super.findByNamedQueryAndNamedParam(queryName, paramNames, values);
   }

   @SuppressWarnings("rawtypes")
   @Override
   @Profiled(
             tag = "hibernate.findByNamedQueryAndValueBean_{$0}",
             message = "findByNamedQueryAndValueBean called with parameters[queryName={$0} valueBean={$1}] and returned {$return.size} elements")
   public List findByNamedQueryAndValueBean(String queryName, Object valueBean)
            throws DataAccessException {
      return super.findByNamedQueryAndValueBean(queryName, valueBean);
   }

   @SuppressWarnings("rawtypes")
   @Override
   @Profiled(
             tag = "hibernate.findByCriteria",
             message = "findByCriteria returned {$return.size} elements")
   public List findByCriteria(DetachedCriteria criteria) throws DataAccessException {
      return super.findByCriteria(criteria);
   }

   @SuppressWarnings("rawtypes")
   @Override
   @Profiled(
             tag = "hibernate.findByCriteria",
             message = "findByCriteria called with parameters[criteria={$0} firstResult={$1} maxResults={$2}] and returned {$return.size} "
                      + "elements")
   public List findByCriteria(DetachedCriteria criteria, int firstResult, int maxResults)
            throws DataAccessException {
      return super.findByCriteria(criteria, firstResult, maxResults);
   }

   @SuppressWarnings("rawtypes")
   @Override
   @Profiled(
             tag = "hibernate.findByExample",
             message = "findByExample called with parameters[exampleEntity={$0}] and returned {$return.size} elements")
   public List findByExample(Object exampleEntity) throws DataAccessException {
      return super.findByExample(exampleEntity);
   }

   @SuppressWarnings("rawtypes")
   @Override
   @Profiled(
             tag = "hibernate.findByExample",
             message = "findByExample called with parameters[exampleEntity={$0} firstResult={$1} maxResults={$2}] and returned {$return.size} elements")
   public List findByExample(Object exampleEntity, int firstResult, int maxResults)
            throws DataAccessException {
      return super.findByExample(exampleEntity, firstResult, maxResults);
   }

   @Override
   @Profiled(
             tag = "hibernate.bulkUpdate",
             message = "bulkUpdate called with parameters[queryString={$0}] and rows affected {$return}")
   public int bulkUpdate(String queryString) throws DataAccessException {
      return super.bulkUpdate(queryString);
   }

   @Override
   @Profiled(
             tag = "hibernate.bulkUpdate",
             message = "bulkUpdate called with parameters[queryString={$0} and value={$1}] and rows affected {$return}")
   public int bulkUpdate(String queryString, Object value) throws DataAccessException {
      return super.bulkUpdate(queryString, value);
   }

   @Override
   @Profiled(
             tag = "hibernate.bulkUpdate",
             message = "bulkUpdate called with parameters[queryString={$0} and values={$1}] and rows affected {$return}")
   public int bulkUpdate(String queryString, Object... values) throws DataAccessException {
      return super.bulkUpdate(queryString, values);
   }

   /**
    * check if JPA is used in fact
    * 
    * @throws IllegalStateException
    *            if native hibernate is used instead of JPA
    */
   protected void ensureJpaIsUsed() throws IllegalStateException {
      if (jpaTemplate == null) {
         throw new IllegalStateException(
                  "before using JPA - turn on scale4j.use.jpa=true property in your configuration files");
      }
   }
}
